skr canvas
![Downloads](https://img.shields.io/npm/dm/@napi-rs/canvas.svg?sanitize=true)
π Help me to become a full-time open-source developer by sponsoring me on Github
Google Skia binding to Node.js via Node-API, 0 System dependencies!
β οΈ This project is in pre-release stage. And there may be some bugs.
For details on planned features and future direction please refer to the Roadmap.
δΈζζζ‘£
Install
yarn add @napi-rs/canvas
npm install @napi-rs/canvas
Support matrix
System requirement
arm64
cortex-a57 or newer CPU architecture on Linux.
All Apple M chips on macOS.
armv7
cortex-a7 or newer CPU architecture.
glibc
Since Skia relies on the glibc 2.18 API, you need to have at least glibc version >= 2.18 on your system.
AWS Lambda usage
To use this library on Lambda you will need to use a Lambda layer.
You can simply attach a lambda layer by getting an ARN from Canvas-Lambda-Layer
Make sure to exclude @napi-rs/canvas
while bundling your Lambda.
Usage
const { promises } = require('node:fs')
const { join } = require('node:path')
const { createCanvas, loadImage } = require('@napi-rs/canvas')
const canvas = createCanvas(300, 320)
const ctx = canvas.getContext('2d')
ctx.lineWidth = 10
ctx.strokeStyle = '#03a9f4'
ctx.fillStyle = '#03a9f4'
ctx.strokeRect(75, 140, 150, 110)
ctx.fillRect(130, 190, 40, 60)
ctx.beginPath()
ctx.moveTo(50, 140)
ctx.lineTo(150, 60)
ctx.lineTo(250, 140)
ctx.closePath()
ctx.stroke()
async function main() {
const catImage = await loadImage('path/to/cat.png')
const dogImage = await loadImage('https://example.com/path/to/dog.jpg')
ctx.drawImage(catImage, 0, 0, catImage.width, catImage.height)
ctx.drawImage(dogImage, canvas.width / 2, canvas.height / 2, dogImage.width, dogImage.height)
const pngData = await canvas.encode('png')
await promises.writeFile(join(__dirname, 'simple.png'), pngData)
}
main()
![](https://github.com/Brooooooklyn/canvas/raw/HEAD/./example/simple.png)
Emoji text
const { writeFileSync } = require('fs')
const { join } = require('path')
const { createCanvas, GlobalFonts } = require('@napi-rs/canvas')
GlobalFonts.registerFromPath(join(__dirname, '..', 'fonts', 'AppleColorEmoji@2x.ttf'), 'Apple Emoji')
GlobalFonts.registerFromPath(join(__dirname, '..', '__test__', 'fonts', 'COLRv1.ttf'), 'COLRv1')
console.info(GlobalFonts.families)
const canvas = createCanvas(760, 360)
const ctx = canvas.getContext('2d')
ctx.font = '50px Apple Emoji'
ctx.strokeText('ππππππ
ππ€£βΊοΈπππ', 50, 150)
ctx.font = '100px COLRv1'
ctx.fillText('abc', 50, 300)
const b = canvas.toBuffer('image/png')
writeFileSync(join(__dirname, 'draw-emoji.png'), b)
![](https://github.com/Brooooooklyn/canvas/raw/HEAD/./example/draw-emoji.png)
Performance
See benchmark for benchmark code.
Hardware info:
,MMMM. Host - xxxxxxxxxxxxxxxxxxxxxxx
.MMMMMM Machine - Mac15,9
MMMMM, Kernel - 24.0.0
.;MMMMM:' MMMMMMMMMM;. OS - macOS 15.0.1 Sequoia
MMMMMMMMMMMMNWMMMMMMMMMMM: DE - Aqua
.MMMMMMMMMMMMMMMMMMMMMMMMWM. WM - Quartz Compositor
MMMMMMMMMMMMMMMMMMMMMMMMM. Packages - 194 (Homebrew), 32 (cargo)
;MMMMMMMMMMMMMMMMMMMMMMMM: Shell - zsh
:MMMMMMMMMMMMMMMMMMMMMMMM: Terminal - warpterminal (Version v0.2024.10.23.14.49.stable_00)
.MMMMMMMMMMMMMMMMMMMMMMMMM. Resolution - 5120x2880@160fps (as 2560x1440)
MMMMMMMMMMMMMMMMMMMMMMMMMMM. 2992x1934@120fps (as 1496x967)
.MMMMMMMMMMMMMMMMMMMMMMMMMM. 2232x1512@60fps (as 1116x756)
MMMMMMMMMMMMMMMMMMMMMMMM Uptime - 1d 2h 32m
;MMMMMMMMMMMMMMMMMMMM. CPU - Apple M3 Max (16)
.MMMM,. .MMMM,. CPU Load - 16%
Memory - 50.1 GB / 134.2 GB
Battery - 78% & Discharging
Disk Space - 624.0 GB / 994.7 GB
β― yarn bench
Draw a House and export to PNG
βββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ¬ββββββββββ
β (index) β Task name β Latency average (ns) β Latency median (ns) β Throughput average (ops/s) β Throughput median (ops/s) β Samples β
βββββββββββΌββββββββββββββββββΌββββββββββββββββββββββββΌβββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββΌββββββββββ€
β 0 β '@napi-rs/skia' β '14676992.14 Β± 0.68%' β '14602333.00' β '68 Β± 0.59%' β '68' β 69 β
β 1 β 'skia-canvas' β '21167809.17 Β± 2.05%' β '20960021.00 Β± 13646.00' β '47 Β± 1.31%' β '48' β 64 β
β 2 β 'node-canvas' β '16552027.42 Β± 0.70%' β '16451291.50 Β± 2208.50' β '60 Β± 0.62%' β '61' β 64 β
βββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββββββ΄βββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ΄ββββββββββ
Draw Gradient and export to PNG
βββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ¬ββββββββββ
β (index) β Task name β Latency average (ns) β Latency median (ns) β Throughput average (ops/s) β Throughput median (ops/s) β Samples β
βββββββββββΌββββββββββββββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββΌββββββββββ€
β 0 β '@napi-rs/skia' β '15228495.58 Β± 0.53%' β '15146312.50 Β± 1187.50' β '66 Β± 0.48%' β '66' β 66 β
β 1 β 'skia-canvas' β '21725564.41 Β± 2.20%' β '21412520.50 Β± 2104.50' β '46 Β± 1.39%' β '47' β 64 β
β 2 β 'node-canvas' β '17976022.14 Β± 1.53%' β '17563479.50 Β± 5104.50' β '56 Β± 1.38%' β '57' β 64 β
βββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββββββ΄ββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ΄ββββββββββ
Features
Path2D
new Path2D()
new Path2D(path: Path2D)
new Path2D(path: string)
export interface DOMMatrix2DInit {
a: number
b: number
c: number
d: number
e: number
f: number
}
export class Path2D {
constructor(path?: Path2D | string)
addPath(path: Path2D, transform?: DOMMatrix2DInit): void
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
closePath(): void
ellipse(
x: number,
y: number,
radiusX: number,
radiusY: number,
rotation: number,
startAngle: number,
endAngle: number,
anticlockwise?: boolean,
): void
lineTo(x: number, y: number): void
moveTo(x: number, y: number): void
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
rect(x: number, y: number, w: number, h: number): void
op(path: Path2D, operation: PathOp): Path2D
toSVGString(): string
getFillType(): FillType
getFillTypeString(): string
setFillType(type: FillType): void
simplify(): Path2D
asWinding(): Path2D
stroke(stroke?: StrokeOptions): Path2D
transform(transform: DOMMatrix2DInit): Path2D
getBounds(): [left: number, top: number, right: number, bottom: number]
computeTightBounds(): [left: number, top: number, right: number, bottom: number]
trim(start: number, end: number, isComplement?: boolean): Path2D
equals(path: Path2D): boolean
}
PathKit
PathKit
is a toolset for manipulating Path in Skia
, supporting quadratic beziers, cubic beziers and conics.
The main features are.
Path Operation
.op(path, PathOp)
const pathOne = new Path2D(
'M8 50H92C96.4183 50 100 53.5817 100 58V142C100 146.418 96.4183 150 92 150H8C3.58172 150 0 146.418 0 142V58C0 53.5817 3.58172 50 8 50Z',
)
const pathTwo = new Path2D(
'"M58 0H142C146.418 0 150 3.58172 150 8V92C150 96.4183 146.418 100 142 100H58C53.5817 100 50 96.4183 50 92V8C50 3.58172 53.5817 0 58 0Z',
)
pathOne.op(pathTwo, PathOp.Intersect).toSVGString()
- Union, subtract the op path from the first path
- Difference, intersect the two paths
- ReverseDifference, union (inclusive-or) the two paths
- Intersect, exclusive-or the two paths
- XOR, subtract the first path from the op path
![boolean-operations](https://github.com/Brooooooklyn/canvas/raw/HEAD/./docs/imgs/boolean-operations.svg)
Covert FillType
in Path
.asWinding()
You can convert fill-rule="evenodd"
to fill-rule="nonzero"
in SVG.
This is useful for OpenType font-related tools, as fill-rule="nonzero"
is only supported in OpenType fonts.
![SVG fill-rule](https://github.com/Brooooooklyn/canvas/raw/HEAD/./docs/imgs/asWinding@2x.png)
const pathCircle = new Path2D(
'M24.2979 13.6364H129.394V40.9091H24.2979L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455H70.4673V68.1818H16.073C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727V113.636C7.00728 118.657 11.0661 122.727 16.073 122.727H70.4673V150H84.0658V122.727H128.041C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818H84.0658V54.5455H133.927C138.934 54.5455 142.993 50.4755 142.993 45.4545V9.09091C142.993 4.07014 138.934 0 133.927 0H21.9592ZM125.702 109.091H20.6058V81.8182H125.702L135.372 95.4545L125.702 109.091Z',
)
pathCircle.setFillType(FillType.EvenOdd)
pathCircle.asWinding().toSVGString()
Simplify Path
.simplify()
Set the path to the same non-overlapping contour as the original path area, which means that it can also remove overlapping paths.
SVG with overlapping paths (Left)
const path =
'M2.933,89.89 L89.005,3.818 Q90.412,2.411 92.249,1.65 Q94.087,0.889 96.076,0.889 Q98.065,0.889 99.903,1.65 Q101.741,2.411 103.147,3.818 L189.22,89.89 Q190.626,91.296 191.387,93.134 Q192.148,94.972 192.148,96.961 Q192.148,98.95 191.387,100.788 Q190.626,102.625 189.219,104.032 Q187.813,105.439 185.975,106.2 Q184.138,106.961 182.148,106.961 Q180.159,106.961 178.322,106.2 Q176.484,105.439 175.077,104.032 L89.005,17.96 L96.076,10.889 L103.147,17.96 L17.075,104.032 Q15.668,105.439 13.831,106.2 Q11.993,106.961 10.004,106.961 Q8.015,106.961 6.177,106.2 Q4.339,105.439 2.933,104.032 Q1.526,102.625 0.765,100.788 Q0.004,98.95 0.004,96.961 Q0.004,94.972 0.765,93.134 Q1.526,91.296 2.933,89.89 Z'
path.simplify().toSVGString()
The tiger.json was serialized from gojs/samples/tiger
node example/anime-girl.js
Building
Build skia from source
You can build this project from source, the system requirements are here: https://skia.org/docs/user/build
$ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
$ cd canvas
$ node scripts/build-skia.js
$ npm install -g yarn
$ yarn install --mode=skip-build
$ sudo dnf install clang
$ yarn build
$ yarn test
$ node example/tiger.js
Pull pre-build skia binary from GitHub
You can pull skia pre-build binaries if you just care the Rust
part:
$ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
$ cd canvas
$ node scripts/release-skia-binary.mjs --download
$ npm install -g yarn
$ yarn install --mode=skip-build
$ sudo dnf install clang
$ yarn build
$ yarn test
$ node example/tiger.js